package net.rvb1.gwt.grad.ui.client.cellview.cell;

import com.google.gwt.cell.client.AbstractInputCell;
import com.google.gwt.cell.client.AbstractSafeHtmlCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesUtils;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
import com.google.gwt.text.shared.SafeHtmlRenderer;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.TextColumn;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;

import net.rvb1.gwt.grad.ui.client.cellview.grid.DataGrid;
import net.rvb1.gwt.grad.ui.client.cellview.grid.DataGrid.Style;
import net.rvb1.gwt.grad.ui.client.cellview.pager.ScrollPager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A {@link Cell} used to render a drop-down list.
 * 
 * @param <T> The type that this Cell represents.
 */
public class SelectionCell<T> extends AbstractInputCell<T, SelectionCell.ViewData> {

  /**
   * Resources used by the datagrid displayed in the cell's popup.
   */
  interface DataGridResources extends net.rvb1.gwt.grad.ui.client.cellview.grid.DataGrid.Resources {
    @Source("SelectionCellDataGrid.css")
    @Override
    public Style dataGridStyle();
  }

  /**
   * The resources used by this example.
   */
  interface Resources extends ClientBundle {
    @Source("SelectionCell-InputBackground.png")
    @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
    ImageResource inputBackground();

    @Source("SelectionCell-MouseOut.png")
    ImageResource menuMouseOut();

    @Source("SelectionCell-MouseOver.png")
    ImageResource menuMouseOver();

    /** Get the styles used but this cell. */
    @Source("SelectionCell.css")
    Styles styles();
  }

  /**
   * The CSS Resources used by this example.
   */
  interface Styles extends CssResource {
    /**
     * Applied to the input element.
     */
    String gradSelectionCellInputElement();

    String gradSelectionCellMenuOut();

    String gradSelectionCellMenuOver();

    String gradSelectionCellPopupPanel();
  }

  interface Template extends SafeHtmlTemplates {
    @SafeHtmlTemplates.Template("<input class=\"{4}\" type=\"text\" style=\"{0}{1}\" selectionCellValue=\"{2}\" value=\"{3}\"/>")
    SafeHtml inputElement(SafeStyles displayStyle, SafeStyles inputStyle,
        Integer selectionCellValue, String textToDisplay, String className);
  }

  static class ViewData {
    private int index;

    private boolean isEditing;

    public ViewData(int index) {
      this.index = index;
    }

    public int getIndex() {
      return this.index;
    }

    public boolean isEditing() {
      return this.isEditing;
    }

    public void setEditing(boolean isEditing) {
      this.isEditing = isEditing;
    }
  }

  private class LabelSafeHtmlCell extends AbstractSafeHtmlCell<T> {
    private LabelSafeHtmlCell(SafeHtmlRenderer<T> renderer, String[] consumedEvents) {
      super(renderer, consumedEvents);
    }

    @Override
    protected void render(Context context, SafeHtml data, SafeHtmlBuilder sb) {
      if (data != null) {
        sb.append(data);
      }
    }
  }

  private class LabelSafeHtmlRenderer extends AbstractSafeHtmlRenderer<T> {
    @Override
    public SafeHtml render(T object) {
      final String label = SelectionCell.this.labelProvider.getLabel(object);
      /*
       * Render a blank space to force the rendered element to have a height. Otherwise it is not
       * clickable.
       */
      return label == null || label.trim().length() == 0 ? SafeHtmlUtils
          .fromTrustedString("\u00A0") : SafeHtmlUtils.fromString(label);
    }
  }

  private static PopupPanel popupPanel;

  private static Template template;

  private boolean allowNull;

  // private DataGrid<T> dataGrid;
  private CellList<T> cellList;

  private DataGrid<T> dataGrid;

  private DataGridResources dataGridResources;

  private SelectionCellProvider<T> labelProvider;

  private Map<Object, List<T>> optionsForKey = new HashMap<Object, List<T>>();

  private ScrollPager pager = null;

  private Resources resources;

  private int width = 150;

  /**
   * Construct a new {@link SelectionCell} with the specified options.
   * 
   * @param options the options in the cell
   */
  public SelectionCell(SelectionCellProvider<T> labelProvider, boolean allowNullValue) {
    super(BrowserEvents.CLICK, BrowserEvents.DBLCLICK, BrowserEvents.MOUSEOVER,
        BrowserEvents.MOUSEOUT);
    if (template == null) {
      template = GWT.create(Template.class);
    }

    this.labelProvider = labelProvider;
    this.allowNull = allowNullValue;

    this.resources = GWT.<Resources> create(Resources.class);
    this.resources.styles().ensureInjected();

    // FIXME (hb): I'm sure this is the good way to do this ...
    // We hide the popup panel when a scroll event happens on screen.
    // Shouldn't a scroll somewhere o n the screen should generate a corresponding BLUR event in the
    // cell ?
    // Apparently not ...
    // Is DOMMouseScroll valid only in firefox or not ?
    // This is too much tricky here !
    Event.addNativePreviewHandler(new NativePreviewHandler() {
      @Override
      public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (popupPanel != null && popupPanel.isShowing()
            && event.getNativeEvent().getType().equals("DOMMouseScroll")) {
          popupPanel.hide(true);
        }
      }
    });
  };

  public int getWidth() {
    return this.width;
  };

  @Override
  public boolean isEditing(Cell.Context context, Element parent, T value) {
    ViewData viewData = getViewData(context.getKey());
    return viewData == null ? false : viewData.isEditing();
  };

  @Override
  public void onBrowserEvent(Cell.Context context, Element parent, T value, NativeEvent event,
      ValueUpdater<T> valueUpdater) {
    super.onBrowserEvent(context, parent, value, event, valueUpdater);
    String type = event.getType();
    Object key = context.getKey();
    final InputElement inputElement = InputElement.as(getInputElement(parent));
    final Element arrowDivElement = getArrowDivElement(parent);
    Element target = event.getEventTarget().cast();

    if ((BrowserEvents.CLICK.equals(type) || BrowserEvents.DBLCLICK.equals(type))
        && arrowDivElement.isOrHasChild(target)) {
      if (!isEditing(context, parent, value)) {
        final String selectionCellValue = inputElement.getAttribute("selectionCellValue");
        ViewData viewData = new ViewData(Integer.parseInt(selectionCellValue));
        viewData.setEditing(true);
        setViewData(key, viewData);
        edit(context, parent, value, valueUpdater);
      } else {
        finishEditing(parent, value, key, valueUpdater);
      }
    } else if (BrowserEvents.MOUSEOVER.equals(type)) {
      inputElement.addClassName("grad-SelectionCell-InputElement-MouseOver");
      if (arrowDivElement.isOrHasChild(target)) {
        arrowDivElement.addClassName(this.resources.styles().gradSelectionCellMenuOver());
      }
    } else if (BrowserEvents.MOUSEOUT.equals(type) && !isEditing(context, parent, value)) {
      inputElement.removeClassName("grad-SelectionCell-InputElement-MouseOver");
      if (arrowDivElement.isOrHasChild(target)) {
        arrowDivElement.removeClassName(this.resources.styles().gradSelectionCellMenuOver());
      }
    }
  }

  @Override
  public void render(Cell.Context context, T value, SafeHtmlBuilder sb) {
    // Get the view data.
    Object key = context.getKey();
    final ViewData viewData = getViewData(key);
    Integer viewDataIndex = (viewData != null) ? viewData.getIndex() : null;

    // Clear the view data if needed
    // Normalement si viewData n'est pas NULL alors cela veut dire que
    // optionsForKey.get(context.getKey()) est obligatoirement non null
    // (car on est déjà passé dans cette méthode avec viewData == null au
    // premier render pour cette clé de contexte)
    if (viewDataIndex != null
        && viewDataIndex.equals(this.optionsForKey.get(context.getKey()).indexOf(value))) {
      clearViewData(key);
      viewDataIndex = null;
    }

    // Get the list of available options from the provider
    List<T> options = new ArrayList<T>(this.labelProvider.getOptions(context));

    Integer selectedIndex;
    if (viewDataIndex == null) {
      selectedIndex = options.indexOf(value);
      if (this.allowNull) {
        selectedIndex += 1;
      }
    } else {
      selectedIndex = viewDataIndex;
    }

    int width = this.width;
    String style = "";
    style += "width:" + width + "px;";
    width -= this.resources.menuMouseOver().getWidth();
    SafeStyles inputStyles = SafeStylesUtils.fromTrustedString("width:" + width + "px;");

    sb.appendHtmlConstant("<div style='" + style + "'>");
    sb.appendHtmlConstant("<table cellpadding=0 cellspacing=0><tr><td>");

    // On écrit l'input de base ...
    final String valueLabel = this.labelProvider.getLabel(value);
    sb.append(template.inputElement(SafeStylesUtils.forDisplay(Display.INLINE_BLOCK), inputStyles,
        selectedIndex, valueLabel != null ? valueLabel : "", this.resources.styles()
            .gradSelectionCellInputElement()));

    sb.appendHtmlConstant("</td>");
    sb.appendHtmlConstant("<td><div class='" + this.resources.styles().gradSelectionCellMenuOut()
        + "' /></td>");
    sb.appendHtmlConstant("</table>");
    sb.appendHtmlConstant("</div>");

    // Save the provided options in a placeholder
    this.optionsForKey.put(context.getKey(), options);
  };

  public void setWidth(int width) {
    this.width = width;
  }

  /**
   * Convert the cell to edit mode.
   * 
   * @param context the {@link Context} of the cell
   * @param parent the parent element
   * @param value the current value
   */
  protected void edit(final Context context, final Element parent, final T value,
      final ValueUpdater<T> valueUpdater) {
    setValue(context, parent, value);
    InputElement input = InputElement.as(getInputElement(parent)); // ????? j'ai du rajouter
                                                                   // InputElement.as(
    input.focus();
    input.select();

    if (popupPanel == null) {
      popupPanel = new PopupPanel(true);
      popupPanel.setStyleName(this.resources.styles().gradSelectionCellPopupPanel());
    }

    if (this.dataGrid == null) {
      DockLayoutPanel dockLayoutPanel = new DockLayoutPanel(Unit.PX);
      this.dataGrid = new DataGrid<T>(20, GWT.<DataGridResources> create(DataGridResources.class));// this.dataGridResources);
      // this.cellList =
      // new CellList<T>(new LabelSafeHtmlCell(new LabelSafeHtmlRenderer(), new String[] {}));
      // ScrollPanel scrollPanel = new ScrollPanel(this.cellList);
      // popupPanel.setWidget(scrollPanel);
      pager = new ScrollPager();
      pager.setDisplay(dataGrid);
      dockLayoutPanel.addSouth(new ScrollPager(), 25);
      dockLayoutPanel.add(this.dataGrid);
      popupPanel.setWidget(dockLayoutPanel);
      // absolutePanel.add(this.cellList);
      // absolutePanel.setWidgetPosition(this.cellList, 0, 0);

      // TODO: donner les bonnes tailles !!
      popupPanel.setWidth(parent.getFirstChildElement().getClientWidth() + "px");
      popupPanel.setHeight("200px");
      // this.cellList.setWidth(parent.getFirstChildElement().getClientWidth() + "px");
      // this.cellList.setHeight("200px");
      this.dataGrid.setWidth(parent.getFirstChildElement().getClientWidth() + "px");
      this.dataGrid.setHeight("200px");

      this.dataGrid.addColumn(new TextColumn<T>() {
        @Override
        public String getValue(T object) {
          final String label = SelectionCell.this.labelProvider.getLabel(object);
          /*
           * Render a blank space to force the rendered element to have a height. Otherwise it is
           * not clickable.
           */
          return label == null || label.trim().length() == 0 ? "\u00A0" : label;
        }
      });

      popupPanel.addCloseHandler(new CloseHandler<PopupPanel>() {
        @Override
        public void onClose(CloseEvent<PopupPanel> event) {
          Element arrowDivElement = getArrowDivElement(parent);
          arrowDivElement.removeClassName(SelectionCell.this.resources.styles()
              .gradSelectionCellMenuOver());

          if (event.isAutoClosed()) {
            finishEditing(parent, value, context.getKey(), valueUpdater);
          }

        }
      });
    }

    List<T> finalList = new ArrayList<T>();
    if (this.allowNull) {
      finalList.add(null);
    }
    finalList.addAll(this.optionsForKey.get(context.getKey()));

    net.rvb1.gwt.grad.ui.client.cellview.pager.ScrollPager.ListDataProvider<T> listDataProvider =
        new net.rvb1.gwt.grad.ui.client.cellview.pager.ScrollPager.ListDataProvider<T>(pager,
            finalList);
    listDataProvider.addDataDisplay(this.dataGrid);
    this.dataGrid.setPageSize(20);
    // listDataProvider.addDataDisplay(this.cellList);
    // this.cellList.setPageSize(finalList.size());
    listDataProvider.flush();

    final SingleSelectionModel<T> selectionModel = new SingleSelectionModel<T>();
    this.dataGrid.setSelectionModel(selectionModel);
    // this.cellList.setSelectionModel(selectionModel);

    if (value != null) {
      selectionModel.setSelected(value, true);
      int indexOf =
          SelectionCell.this.optionsForKey.get(context.getKey()).indexOf(
              selectionModel.getSelectedObject());
      if (SelectionCell.this.allowNull) {
        if (indexOf == -1) {
          indexOf = 0;
        } else {
          indexOf = indexOf + 1;
        }
      }

      DOM.scrollIntoView((com.google.gwt.user.client.Element) com.google.gwt.user.client.Element
          .as(SelectionCell.this.dataGrid.getRowElement(indexOf)));

      // DOM.scrollIntoView((com.google.gwt.user.client.Element) com.google.gwt.user.client.Element
      // .as(SelectionCell.this.cellList.getRowElement(indexOf)));
    }

    // schedule finally car l'ordre setSelected au dessus envoie un évènement que l'on ne veut pas
    // catcher tout de suite ...
    Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
      @Override
      public void execute() {
        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
          @Override
          public void onSelectionChange(SelectionChangeEvent event) {
            popupPanel.hide(false);
            pager.resetLastScrollPos();
            getInputElement(parent).removeClassName("grad-SelectionCell-InputElement-MouseOver");
            finishEditing(parent, selectionModel.getSelectedObject(), context.getKey(),
                valueUpdater);
          }
        });
      }
    });
    popupPanel.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
      @Override
      public void setPosition(int offsetWidth, int offsetHeight) {
        // TODO : -1 car border de 1px dans les styles ... récupérer cette valeur dans la css
        popupPanel.setPopupPosition(parent.getFirstChildElement().getAbsoluteLeft(), parent
            .getFirstChildElement().getAbsoluteTop()
            - 1 + parent.getOffsetHeight());
      }
    });
  }

  @Override
  protected void finishEditing(Element parent, T valueToDisplay, Object key,
      com.google.gwt.cell.client.ValueUpdater<T> valueUpdater) {

    Integer newValueAsInt = this.optionsForKey.get(key).indexOf(valueToDisplay);

    final String label = this.labelProvider.getLabel(valueToDisplay);

    InputElement inputElement = InputElement.as(getInputElement(parent));
    inputElement.setValue(label != null ? label : "");
    inputElement.setDefaultValue(inputElement.getDefaultValue());
    inputElement.setAttribute("selectionCellValue", newValueAsInt.toString());
    // parent.focus();

    // Get the view data.
    // ViewData vd = getViewData(key);
    // if (vd == null) {
    // vd = new ViewData(newValue);
    // setViewData(key, vd);
    // }
    ViewData vd = new ViewData(newValueAsInt);
    setViewData(key, vd);

    // TODO: Gérer l'update conditionnel avec getViewData
    valueUpdater.update(valueToDisplay);
  }

  protected Element getArrowDivElement(Element parent) {
    return ((TableElement) parent.getFirstChildElement().getFirstChildElement().cast()).getRows()
        .getItem(0).getCells().getItem(1).getFirstChildElement();
  }

  @Override
  protected Element getInputElement(Element parent) {
    return ((TableElement) parent.getFirstChildElement().getFirstChildElement().cast()).getRows()
        .getItem(0).getCells().getItem(0).getFirstChildElement();
  }

}
